﻿Imports System.Text
Imports System.Windows
Imports System.Windows.Controls
Imports System.Windows.Input
Imports Microsoft.VisualStudio.Text
Imports Microsoft.VisualStudio.Text.Classification
Imports Microsoft.VisualStudio.Text.Editor
Imports Microsoft.VisualStudio.Text.Formatting
Imports System.Windows.Media
Imports System.ComponentModel.Composition
Imports Microsoft.VisualStudio.Utilities
Imports System.IO
Imports EnvDTE

Namespace Microsoft.VisualStudio.ImageInsertion
	''' <summary>
	''' Manages the image adornments in an instance of <see cref="IWpfTextView"/>
	''' </summary>
	Friend Class ImageAdornmentManager
		Private WithEvents adornmentVisualElement As EditorImage
		Private Const ImageAdornmentLayerName = "Intra Text Adornment"

		Private serviceProvider As IServiceProvider

		Private Property ImagesAdornmentsRepository As ImageAdornmentRepositoryService

		Friend ReadOnly Property Images As IList(Of ImageAdornment)
			Get
				Return Me.ImagesAdornmentsRepository.Images
			End Get
		End Property

		Friend Property HighlightLineAdornment As HighlightLineAdornment

		Friend Property PreviewImageAdornment As PreviewImageAdornment

		Private WithEvents mView As IWpfTextView
		Friend Property View As IWpfTextView
			Get
				Return mView
			End Get
			Private Set(ByVal value As IWpfTextView)
				mView = value
			End Set
		End Property

		Friend Property AdornmentLayer As IAdornmentLayer

		Friend Sub New(ByVal serviceProvider As IServiceProvider, ByVal view As IWpfTextView, ByVal editorFormatMap As IEditorFormatMap)
			Me.View = view
			Me.serviceProvider = serviceProvider
			Me.AdornmentLayer = Me.View.GetAdornmentLayer(ImageAdornmentLayerName)

			Me.ImagesAdornmentsRepository = New ImageAdornmentRepositoryService(view.TextBuffer)

			' Create the highlight line adornment
			Me.HighlightLineAdornment = New HighlightLineAdornment(view, editorFormatMap)
			Me.AdornmentLayer.AddAdornment(AdornmentPositioningBehavior.OwnerControlled, Nothing, HighlightLineAdornment, HighlightLineAdornment.VisualElement, Nothing)

			' Create the preview image adornment
			Me.PreviewImageAdornment = New PreviewImageAdornment
			Me.AdornmentLayer.AddAdornment(AdornmentPositioningBehavior.OwnerControlled, Nothing, Me, Me.PreviewImageAdornment.VisualElement, Nothing)

			' Attach to the view events
			AddHandler Me.View.TextBuffer.Changed, AddressOf OnBufferChanged

			' Load and initialize the image adornments repository
			Me.ImagesAdornmentsRepository.Load()
			Me.ImagesAdornmentsRepository.Images.ToList().ForEach(Sub(image) InitializeImageAdornment(image))
		End Sub

		Private Sub OnViewClosed() Handles mView.Closed
			' Save the image adornments
			Me.ImagesAdornmentsRepository.Save()

			' Detach from the view events
			RemoveHandler Me.View.TextBuffer.Changed, AddressOf OnBufferChanged
		End Sub

		Private Sub OnBufferChanged(ByVal sender As Object, ByVal e As TextContentChangedEventArgs)
			' Remove the image adornments if the associated spans were deleted.
			Dim imagesToBeRemoved As New List(Of ImageAdornment)
			For Each imageAdornment In Me.ImagesAdornmentsRepository.Images
				Dim span = imageAdornment.TrackingSpan.GetSpan(e.After)
				If span.Length = 0 Then
					imagesToBeRemoved.Add(imageAdornment)
				End If
			Next imageAdornment

			imagesToBeRemoved.ForEach(Sub(imageAdornment) RemoveImageAdornment(imageAdornment))
		End Sub

		Private Sub OnLayoutChanged(ByVal sender As Object, ByVal e As TextViewLayoutChangedEventArgs) Handles mView.LayoutChanged
			Dim imageAdornmentsToBeShown As New List(Of ImageAdornment)

			' Detect which images should be shown again based on the new or reformatted spans
			For Each span In e.NewOrReformattedSpans
				Dim localSpan = span
				imageAdornmentsToBeShown.AddRange(Me.ImagesAdornmentsRepository.Images.Where(Function(image) image.TrackingSpan.GetSpan(Me.View.TextSnapshot).OverlapsWith(localSpan)))
			Next span

			For Each imageAdornment In imageAdornmentsToBeShown
				Dim imageSnaphotSpan = imageAdornment.TrackingSpan.GetSpan(Me.View.TextSnapshot)
				' Get the text view line associated with the image span
				Dim newOrReformattedLine = e.NewOrReformattedLines.FirstOrDefault(Function(line) line.ContainsBufferPosition(imageSnaphotSpan.Start) AndAlso line.ContainsBufferPosition(imageSnaphotSpan.End))
				If newOrReformattedLine IsNot Nothing Then
					' Use the top of the text view line to set image top location. And finally adjust the final location using the delta Y.
					Canvas.SetTop(imageAdornment.VisualElement, newOrReformattedLine.Top + imageAdornment.TextViewLineDelta.Y)
					Show(imageAdornment, newOrReformattedLine)
				End If
			Next imageAdornment
		End Sub

		Friend Function GetTargetTextViewLine(ByVal uiElement As UIElement) As ITextViewLine
			If Me.View.TextViewLines Is Nothing Then
				Return Nothing
			End If

			Return Me.View.TextViewLines.GetTextViewLineContainingYCoordinate(Canvas.GetTop(uiElement))
		End Function

		Friend Function GetTargetTextViewLine(ByVal imageAdornment As ImageAdornment) As ITextViewLine
			If Me.View.TextViewLines Is Nothing Then
				Return Nothing
			End If

			Return Me.View.TextViewLines.GetTextViewLineContainingBufferPosition(imageAdornment.TrackingSpan.GetStartPoint(Me.View.TextSnapshot))
		End Function

		''' <summary>
		''' Creates and adds an image adornment for the image.
		''' </summary>
		''' <param name="image"></param>
		''' <returns></returns>
		Friend Function AddImageAdornment(ByVal image As Image) As ImageAdornment
			Dim targetLine = GetTargetTextViewLine(image)

			If targetLine IsNot Nothing AndAlso targetLine.Length > 0 Then
				Dim imageAdornment As New ImageAdornment(New SnapshotSpan(targetLine.Start, targetLine.Length), image)

				' Initialize the image adornment
				InitializeImageAdornment(imageAdornment)

				' Add the image adornment to the repository
				ImagesAdornmentsRepository.Add(imageAdornment)
				ImagesAdornmentsRepository.EnsureRepositoryFileExists()

				' Add the repository file to the solution explorer
				Me.AddFileToTheActiveDocument(Me.ImagesAdornmentsRepository.RepositoryFilename)

				' Show the image
				Show(imageAdornment)

				DisplayTextViewLine(imageAdornment)

				Return imageAdornment
			End If

			Return Nothing
		End Function

		Private Sub AddFileToTheActiveDocument(ByVal filename As String)
			If Not String.IsNullOrEmpty(filename) AndAlso File.Exists(filename) Then
				If Me.serviceProvider Is Nothing Then
					Dim vs = TryCast(Me.serviceProvider.GetService(GetType(DTE)), DTE)
					If Not vs Is Nothing AndAlso Not vs.ActiveDocument Is Nothing Then
						Dim projectItem = vs.ActiveDocument.ProjectItem.ProjectItems.AddFromFile(filename)
						If Not projectItem Is Nothing Then
							Dim buildActionProperty = projectItem.Properties.Item("BuildAction")
							If Not buildActionProperty Is Nothing Then
								buildActionProperty.Value = 0
							End If
						End If
					End If
				End If

			End If
		End Sub

		Private Sub InitializeImageAdornment(ByVal imageAdornment As ImageAdornment)
			adornmentVisualElement = imageAdornment.VisualElement
		End Sub

		Private Sub OnAdornmentVisualElementDeleted(ByVal sender As Object, ByVal e As EventArgs) Handles adornmentVisualElement.Deleted
			Dim visualElement = TryCast(sender, EditorImage)
			Dim imageAdornment = TryCast(visualElement.Tag, ImageAdornment)

			RemoveImageAdornment(imageAdornment)

		End Sub

		Private Sub RemoveImageAdornment(ByVal imageAdornment As ImageAdornment)
			Me.ImagesAdornmentsRepository.Remove(imageAdornment)
			Me.AdornmentLayer.RemoveAdornment(imageAdornment.VisualElement)
		End Sub

		Private Sub OnAdornmentVisualElementMouseMove(ByVal sender As Object, ByVal e As MouseEventArgs) Handles adornmentVisualElement.MouseMove
			Dim visualElement = TryCast(sender, FrameworkElement)
			Dim imageAdornment = TryCast(visualElement.Tag, ImageAdornment)

			If e.LeftButton = MouseButtonState.Pressed Then
				' Move the image adornment
				If Not imageAdornment.VisualElement.IsResizing AndAlso (Not Me.ImagesAdornmentsRepository.Images.ToList().Exists(Function(image) image IsNot imageAdornment AndAlso image.VisualElement.IsMoving)) Then
					imageAdornment.VisualElement.IsMoving = True

					Dim adjustedPosition = e.GetPosition(Me.View.VisualElement)
					adjustedPosition.X += Me.View.ViewportLeft - (imageAdornment.VisualElement.Width / 2)
					adjustedPosition.Y += Me.View.ViewportTop - (imageAdornment.VisualElement.Height / 2)

					Me.AdornmentLayer.RemoveAdornmentsByTag(imageAdornment)

					imageAdornment.VisualElement.Opacity = PreviewImageAdornment.PreviewOpacity
					imageAdornment.VisualElement.MoveTo(adjustedPosition)

					Show(imageAdornment)

					Me.HighlightLineAdornment.Highlight(GetTargetTextViewLine(imageAdornment))
				End If
			End If

			e.Handled = True
		End Sub

		Private Sub OnAdornmentVisualElementMouseLeave(ByVal sender As Object, ByVal e As MouseEventArgs) Handles adornmentVisualElement.MouseLeave
			Dim visualElement = TryCast(sender, FrameworkElement)
			Dim imageAdornment = TryCast(visualElement.Tag, ImageAdornment)
			imageAdornment.VisualElement.Opacity = 1
			imageAdornment.VisualElement.IsMoving = False
			Me.HighlightLineAdornment.Clear()
		End Sub

		Private Sub OnAdornmentVisualElementMouseLeftButtonDown(ByVal sender As Object, ByVal e As MouseButtonEventArgs) Handles adornmentVisualElement.MouseLeftButtonDown
			Dim visualElement = TryCast(sender, FrameworkElement)
			Dim imageAdornment = TryCast(visualElement.Tag, ImageAdornment)
			Me.HighlightLineAdornment.Highlight(GetTargetTextViewLine(imageAdornment))
			e.Handled = True
		End Sub

		Private Sub OnAdornmentVisualElementMouseLeftButtonUp(ByVal sender As Object, ByVal e As MouseButtonEventArgs) Handles adornmentVisualElement.MouseLeftButtonUp
			Dim visualElement = TryCast(sender, FrameworkElement)
			Dim imageAdornment = TryCast(visualElement.Tag, ImageAdornment)
			imageAdornment.VisualElement.Opacity = 1
			Me.HighlightLineAdornment.Clear()
			Me.DisplayTextViewLine(imageAdornment)
		End Sub

		Private Sub Show(ByVal imageAdornment As ImageAdornment)
			Show(imageAdornment, GetTargetTextViewLine(imageAdornment))
		End Sub

		Private Sub Show(ByVal imageAdornment As ImageAdornment, ByVal targetTextViewLine As ITextViewLine)
			If targetTextViewLine IsNot Nothing Then
				' Update the line delta
				imageAdornment.TextViewLineDelta = New Point(imageAdornment.VisualElement.Left - targetTextViewLine.Left, imageAdornment.VisualElement.Top - targetTextViewLine.Top)
			End If

			Me.AdornmentLayer.RemoveAdornmentsByTag(imageAdornment)
			Me.AdornmentLayer.AddAdornment(AdornmentPositioningBehavior.TextRelative, imageAdornment.TrackingSpan.GetSpan(Me.View.TextSnapshot), imageAdornment, imageAdornment.VisualElement, Nothing)

			UpdateTargetLocation(imageAdornment)
		End Sub

		Private Sub OnImageAdornmentResizing(ByVal sender As Object, ByVal e As EventArgs) Handles adornmentVisualElement.Resizing
			If Me.View.TextViewLines IsNot Nothing Then
				Dim editorImage = TryCast(sender, EditorImage)
				Dim imageAdornment = TryCast(editorImage.Tag, ImageAdornment)
				DisplayTextViewLine(imageAdornment)
			End If
		End Sub

		Private Sub DisplayTextViewLine(ByVal imageAdornment As ImageAdornment)
			Dim textViewLine = Me.View.TextViewLines.FirstOrDefault(Function(line) imageAdornment.ApplyRenderTrackingPoint(Me.View.TextSnapshot, line))

			If textViewLine IsNot Nothing Then
				Me.View.DisplayTextLineContainingBufferPosition(textViewLine.Start, textViewLine.Top, ViewRelativePosition.Top)
			Else
				Me.View.DisplayTextLineContainingBufferPosition(New SnapshotPoint(Me.View.TextSnapshot, 0), 0, ViewRelativePosition.Top)
			End If
		End Sub

		Private Sub UpdateTargetLocation(ByVal imageAdornment As ImageAdornment)
			imageAdornment.RenderTrackingPoint = Nothing

			For Each line As ITextViewLine In Me.View.TextViewLines
				Dim lineArea As New Rect(line.Left, line.Top, line.Width, line.Height)
				Dim imageAdornmentArea = imageAdornment.VisualElement.Area
				' Use the height half to be able to move the image up and down
				imageAdornmentArea.Height = imageAdornmentArea.Height / 2

				If line.Length > 0 AndAlso lineArea.IntersectsWith(imageAdornmentArea) Then
					imageAdornment.RenderTrackingPoint = Me.View.TextSnapshot.CreateTrackingPoint(line.Start.Position, PointTrackingMode.Negative)
					imageAdornment.UpdateTrackingSpan(line)

					Return
				End If
			Next line
		End Sub
	End Class
End Namespace